Skip to content

feat(json-schema): resolve standalone JSON Schema 2020-12 documents - #4307

Merged
robert-hebel-sb merged 14 commits into
mainfrom
feat/json-schema-2020-12
Nov 6, 2026
Merged

robert-hebel-sb merged 14 commits into
mainfrom
feat/json-schema-2020-12

Conversation

@robert-hebel-sb

@robert-hebel-sb robert-hebel-sb commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What

  • New json-schema-2020-12-apidom resolve strategy for documents whose root $schema is https://json-schema.org/draft/2020-12/schema (an empty fragment is normalised) and whose root has none of openapi, swagger, asyncapi and arazzo. Any other document keeps the strategy it had (generic for AsyncAPI/Arazzo, refused $schema values and nested-only $schema).
  • $refs resolve over apidom-reference's json-schema-2020-12 strategies:
    • against $id base URIs
    • to $anchor and $dynamicAnchor plain names
    • to external documents, as in OpenAPI 3.1 Schema Objects
  • Every $dynamicRef is left in place.
  • A $ref from a resolved subtree to one of its ancestors ("$ref": "#") is a cycle, so resolveSubtree, Swagger UI's path, works on recursive schemas.
  • The dereference strategy merges allOf (unless mode: 'strict') and applies modelPropertyMacro, so a schema resolves the same standalone and inside an OpenAPI 3.1 document.
  • The JSON/YAML parsers detect a document by its parsed root and are used only by this strategy. The OpenAPI parser lists are unchanged, and a regression test covers OpenAPI 3.1 external targets with a root 2020-12 $schema.
  • Registered before generic in Swagger.resolve and Swagger.resolveSubtree. The parsers and the dereference strategy are exported in Swagger.apidom.
  • ApiDOM is bumped to 1.13.3 in its own commit. The README, CLAUDE.md and installation docs are updated.

Out of scope: references that cross dialects; bundling; inferring the dialect without $schema. The same ancestor-cycle defect in the OpenAPI 3.1/3.2 strategies is fixed separately in #4304.

Known limitations (apidom-reference, same as OpenAPI 3.1; each pinned by a test.failing fixture):

  • the uri selector misses a relative $id nested in another relative $id (id-relative-nested);
  • the $anchor selector also searches embedded $id resources and takes the first match, so a reused anchor name can select the embedded resource's schema (anchor-embedded-resource).

For Swagger UI: import swagger-client/es/resolver/strategies/json-schema-2020-12-apidom into both strategy lists.

🤖 Generated with Claude Code

RobertHebel and others added 9 commits October 27, 2026 18:09
Move every @swagger-api/apidom-* package to 1.13.3, add
@swagger-api/apidom-ns-json-schema-2020-12 to dependencies (the JSON Schema
2020-12 resolve strategy imports it) and its two parser adapters to
optionalDependencies, mirroring apidom-reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erge)

Install @swagger-api/apidom-reference and
@swagger-api/apidom-ns-json-schema-2020-12 from tarballs packed from the
ApiDOM branch feat/json-schema-2020-12/phase-3 at commit 11951862a, which
adds the json-schema-2020-12 resolve and dereference strategies and exports
keyMap and getNodeType from the namespace. The tarballs live in
../upstream-tarballs; "$" overrides make nested copies use them.

Drop this commit once ApiDOM releases the branch, and bump every
@swagger-api/apidom-* package to that release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the json-schema-2020-12-apidom resolve strategy for documents whose root
$schema is https://json-schema.org/draft/2020-12/schema (an empty fragment
normalised) and which have no openapi or swagger field. Any other $schema
keeps the generic strategy. The strategy resolves over apidom-reference's
json-schema-2020-12 strategies, so $refs resolve against $id base URIs,
$anchor and $dynamicAnchor plain names and external documents as in
OpenAPI 3.1 Schema Objects; every $dynamicRef is left in place.

The json-schema-2020-12-swagger-client dereference strategy carries over the
OpenAPI 3.1 Schema Object handling (cycles, $$ref meta patches, collected
errors), merges allOf unless mode is strict and applies modelPropertyMacro,
so a schema resolves the same standalone and inside an OpenAPI 3.1
document. A $ref in the document root keeps its replacement as the parse
result. The JSON and YAML parsers detect a document by its parsed root and
are used only by this strategy; the OpenAPI parser lists are unchanged.

Register the strategy ahead of generic in Swagger.resolve and
Swagger.resolveSubtree, and export the parsers and the dereference strategy
in Swagger.apidom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record the decisions the 2020-12 work answered (cross-dialect references,
unreleased upstream, content-detecting parsers, $ref to $dynamicAnchor) in
design-and-delivery.md, and in strategy-design.md refract with the
namespace's refract (apidom-ns-arazzo-1 reassigns JSONSchemaElement.refract)
and keep the result class on a dereferenced root.

FOLLOW-UPS: keyMap/getNodeType are exported on the unreleased ApiDOM
branch; new item for apidom-reference's uri selector, which resolves
candidate $id chains against the $ref target instead of the document's
base URI (OpenAPI 3.1 and 3.2 alike).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dereference lineage held only the pathDiscriminator fragment, so a $ref
from the fragment to one of its ancestors was not a cycle: "$ref": "#" in a
recursive schema overflowed the stack when Swagger UI resolved
['properties'] or ['properties', 'children'] through resolveSubtree, and
['properties', 'children', 'items'] returned null. Seed the lineage with
the document root and every element along the pathDiscriminator, so such a
$ref resolves to an absolute $ref, as cycles do everywhere else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A document is standalone JSON Schema only when its root has the 2020-12
$schema and none of the API description fields openapi, swagger, asyncapi
and arazzo. AsyncAPI and Arazzo documents may declare a root $schema; they
keep the generic strategy they had, which regression tests check by
comparing with a generic-only resolve. The parsers detect through the same
predicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apidom-reference's $anchor selector searches embedded $id resources too and
takes the first match, so "#tag" can select the embedded resource's schema
instead of the referencing resource's (JSON Schema Core 8.2.2). OpenAPI 3.1
Schema Objects behave the same. Pin it with a test.failing fixture that
starts failing once ApiDOM stops the search at nested $ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State the matching rule as no openapi, swagger, asyncapi or arazzo field
(D18) wherever it appears, record D18 and D19 in design-and-delivery.md,
require the pathDiscriminator lineage in strategy-design.md, and in
FOLLOW-UPS put 2.3 after 2.2 and add 2.4 for apidom-reference's $anchor
selector, which searches embedded schema resources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@robert-hebel-sb
robert-hebel-sb marked this pull request as draft October 28, 2026 12:53
@robert-hebel-sb
robert-hebel-sb requested a balanced review from Copilot October 29, 2026 09:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@robert-hebel-sb
robert-hebel-sb marked this pull request as ready for review November 4, 2026 13:13
@robert-hebel-sb
robert-hebel-sb requested a max review from Copilot November 4, 2026 13:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The resolver cache reuses a mutated graph, causing subsequent resolutions to retain merged allOf results in strict mode and potentially reuse results from incompatible options.

3 open issues

Review effort: Max

Comment thread package.json Resolved
robert-hebel-sb and others added 3 commits November 4, 2026 14:39
The script linked the local ApiDOM namespaces and core but left the
published apidom-reference in use, so changes to the upstream resolve
and dereference strategies were not exercised during linked development.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Copilot review of PR #4307 found that the circular replace return skips
indirections.pop() and that the resolve strategy cache reuses a graph
that dereferencing mutates. Both patterns come from the OpenAPI 3.1
strategy, so they are recorded as follow-ups for both.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Dereferencing discards sibling constraints when a reference targets true and misclassifies internal references as external when the root $id matches the retrieval URI, causing incorrect resolution-flag behavior.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Ignore fragments when classifying root $id references

src/​resolver/​apidom/​reference/​dereference/​strategies/​json-schema-2020-12-swagger-client/​visitors/​dereference.js:109

When the root $id equals the retrieval URI, a reference such as #/$defs/User succeeds in uriEvaluate without entering the fallback. This comparison still includes the target fragment, so it marks that internal reference as external: resolve.internal: false dereferences it, while resolve.external: false leaves it unresolved. Compare the fragment-free URIs and extend the existing resolution-flag tests with a root $id case.

Medium severity Apply sibling keywords when a $ref targets true

src/​resolver/​apidom/​reference/​dereference/​strategies/​json-schema-2020-12-swagger-client/​visitors/​dereference.js:309

A reference to true currently discards every sibling keyword: with $defs.Any: true, { $ref: '#/$defs/Any', type: 'string' } becomes true instead of retaining the string constraint. JSON Schema 2020-12 applies those siblings alongside $ref. Treat a true target as an empty object schema when siblings exist, so the object-merging path preserves them, and add a regression for this case.

3 resolved since last review

Review effort: Max

normalize was the identity function. Inline it in the strategy, since
resolveSubtree still calls strategy.normalize, and remove the dead
skipNormalization branch from resolve.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@robert-hebel-sb
robert-hebel-sb merged commit c5d7f19 into main Nov 6, 2026
7 checks passed
@robert-hebel-sb
robert-hebel-sb deleted the feat/json-schema-2020-12 branch November 6, 2026 13:23
swagger-bot pushed a commit that referenced this pull request Nov 6, 2026
# [3.39.0](v3.38.4...v3.39.0) (2026-11-06)

### Features

* **json-schema:** resolve standalone JSON Schema 2020-12 documents ([#4307](#4307)) ([c5d7f19](c5d7f19))
@swagger-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.39.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants